﻿using Cognitum.CassandraProviders.Ccc;
using Cognitum.CassandraTableStorage;
using Cognitum.CassandraTableStorage.Entities;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Data.Services.Client;
using System.Linq;
using System.Web.Profile;

namespace Cognitum.CassandraProviders.Cassandra
{
    public class CassandraProfileProvider : ProfileProvider
    {
        #region Structs

        public struct PropertyNames
        {
            #region Fields

            public const string Birthdate = "Birthdate";
            public const string Gender = "Gender";
            public const string PortraitBlobAddressUri = "PortraitBlobAddressUri";
            public const string Tags = "Tags";

            #endregion Fields
        }

        #endregion Structs

        #region | Public Properties |

        public override string ApplicationName { get; set; }

        #endregion | Public Properties |

        #region | Private Fields |

        private string _connectionString;
        private int _userIsActiveTimeWindow;
        private int _userIsOnlineTimeWindow;
        private int _replicationFactor;

        #endregion | Private Fields |

        #region | Public Methods |

        //Active sers have special table. It includes username, applicationName and lastActivityDate.
        //Every inserted rows have the same ttl, so active users are the users, who are in UserEctiveEntity table.
        //UserIsActiveTimeWindow is specified in config file. Method is independent of parameter userInactiveSinceDate, but UserIsActiveTimeWindow.
        public override int DeleteInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            List<string> usernameActiveListsFounded = userDataSource.GetActiveProfiles(ApplicationName);

            IEnumerable<user_entity> userEntities = userDataSource.GetAllProfiles(ApplicationName);

            userEntities = GetInactiveProfiles(userEntities, usernameActiveListsFounded);

            return DeleteProfiles(userEntities.Select(x => x.username).ToArray());
        }

        public override int DeleteProfiles(ProfileInfoCollection profiles)
        {
            string[] usernames = new string[profiles.Count];
            int i = 0;
            foreach (ProfileInfo profile in profiles)
            {
                usernames[i++] = profile.UserName;
            }
            return DeleteProfiles(usernames);
        }

        public override int DeleteProfiles(string[] usernames)
        {
            int counter = 0;
            EncryptionUtilities encryption = new EncryptionUtilities();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            user_entity userEntity;
            foreach (string userName in usernames)
            {
                if (userName == null)
                    throw new ProviderException("Username is wrong.");
                userEntity = userDataSource.GetUser(encryption.GetMd5Hash(userName.ToLower()).ToString(), ApplicationName).FirstOrDefault();
                if (userEntity == null)
                    throw new ProviderException("User not found.");
                userEntity.is_profile_deleted = true;
                userEntity.gender = -1;
                userEntity.last_profile_updated_date = new DateTimeOffset(new DateTime(1970, 1, 1));
                userEntity.birthday = new DateTimeOffset(new DateTime(1970, 1, 1));
                userEntity.portrait_blob_address_uri = String.Empty;
                userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
                try //Delete if is online
                {
                    userDataSource.Delete(new user_online_entity()
                    {
                        application_name = ApplicationName,
                        partition_key = encryption.GetMd5Hash(userName.ToLower()).ToString(),
                        username = userName.ToLower()
                    });
                }
                catch { }
                try //Delete if is active
                {
                    userDataSource.Delete(new user_active_entity()
                    {
                        application_name = ApplicationName,
                        partition_key = encryption.GetMd5Hash(userName.ToLower()).ToString(),
                        username = userName.ToLower()
                    });
                }
                catch { }
                counter++;
            }
            return counter;
        }

        public override ProfileInfoCollection FindInactiveProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            usernameToMatch = usernameToMatch.ToLower();

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            List<string> usernameActiveListsFounded = userDataSource.FindActiveProfilesByUserName(usernameToMatch, ApplicationName);

            IEnumerable<user_entity> userEntities = userDataSource.FindUserProfiles(usernameToMatch, ApplicationName);

            userEntities = GetInactiveProfiles(userEntities, usernameActiveListsFounded);

            ProfileInfoCollection users = new ProfileInfoCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetProfileInfoFromEntity(userEntity));
            }

            return users;
        }

        public override ProfileInfoCollection FindProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            usernameToMatch = usernameToMatch.ToLower();

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.FindUserProfiles(usernameToMatch, ApplicationName);

            ProfileInfoCollection users = new ProfileInfoCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetProfileInfoFromEntity(userEntity));
            }

            return users;
        }

        public override ProfileInfoCollection GetAllInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            List<string> usernameActiveListsFounded = userDataSource.GetActiveProfiles(ApplicationName);

            IEnumerable<user_entity> userEntities = userDataSource.GetAllProfiles(ApplicationName);

            userEntities = GetInactiveProfiles(userEntities, usernameActiveListsFounded);

            ProfileInfoCollection users = new ProfileInfoCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetProfileInfoFromEntity(userEntity));
            }

            return users;
        }

        public override ProfileInfoCollection GetAllProfiles(ProfileAuthenticationOption authenticationOption, int pageIndex, int pageSize, out int totalRecords)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetAllProfiles(ApplicationName);

            ProfileInfoCollection users = new ProfileInfoCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetProfileInfoFromEntity(userEntity));
            }

            return users;
        }

        public override int GetNumberOfInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            int numberOfActiveProfiles = userDataSource.GetNumberActiveProfiles(ApplicationName);

            int numberOfAllUsers = userDataSource.GetNumberAllProfiles(ApplicationName);

            return numberOfAllUsers - numberOfActiveProfiles;
        }

        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
        {
            string userName = ((string)context["UserName"]).ToLower();

            EncryptionUtilities encryption = new EncryptionUtilities();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            user_entity userEntity = userDataSource.GetUser(encryption.GetMd5Hash(userName).ToString(), ApplicationName).FirstOrDefault();

            if (userEntity == null)
                throw new ProviderException("User not found.");

            // The serializeAs attribute is ignored in this provider implementation.
            SettingsPropertyValueCollection svc = new SettingsPropertyValueCollection();

            foreach (SettingsProperty prop in collection)
            {
                SettingsPropertyValue pv = new SettingsPropertyValue(prop);

                switch (prop.Name)
                {
                    case PropertyNames.Gender:
                        pv.PropertyValue = userEntity.gender;
                        break;

                    case PropertyNames.Birthdate:
                        pv.PropertyValue = userEntity.birthday;
                        break;

                    case PropertyNames.PortraitBlobAddressUri:
                        pv.PropertyValue = userEntity.portrait_blob_address_uri;
                        break;

                    default:
                        throw new ProviderException("Unsupported property.");
                }

                svc.Add(pv);
            }

            return svc;
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            // Initialize values from web.config.
            if (config == null)
                throw new ArgumentNullException("config");

            if (String.IsNullOrEmpty(name))
                throw new ArgumentNullException("config");

            // Initialize the abstract base class.
            base.Initialize(name, config);

            _connectionString = GetConfigValue(ConfigurationManager.AppSettings["ConnectionString"], "localhost");

            _userIsActiveTimeWindow = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["UserIsActiveTimeWindow"], "3"));

            ApplicationName = String.IsNullOrEmpty(config["applicationName"]) ? System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath : config["applicationName"];

            if (!int.TryParse(GetConfigValue(ConfigurationManager.AppSettings["UserIsOnlineTimeWindow"], "15"), out _userIsOnlineTimeWindow)) _userIsOnlineTimeWindow = 15;
            _replicationFactor = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["ReplicationFactor"], "1"));
        }

        public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
        {
            // The serializeAs attribute is ignored in this provider implementation.
            string userName = ((string)context["UserName"]).ToLower();

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            EncryptionUtilities encrypt = new EncryptionUtilities();
            user_entity userEntity = userDataSource.GetUser(encrypt.GetMd5Hash(userName).ToString(), ApplicationName).FirstOrDefault();

            if (userEntity == null)
                throw new ProviderException("User not found.");

            foreach (SettingsPropertyValue pv in collection)
            {
                switch (pv.Property.Name)
                {
                    case PropertyNames.Gender:
                        userEntity.gender = (int)pv.PropertyValue;
                        break;

                    case PropertyNames.Birthdate:
                        userEntity.birthday = (DateTimeOffset)pv.PropertyValue;
                        break;

                    case PropertyNames.PortraitBlobAddressUri:
                        userEntity.portrait_blob_address_uri = (string)pv.PropertyValue;
                        break;

                    default:
                        throw new ProviderException("Unsupported property.");
                }
            }

            // Store when profile was updated
            userEntity.is_profile_deleted = false;
            userEntity.last_profile_updated_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
        }

        #endregion | Public Methods |

        #region | Private Methods |

        /// <summary>
        /// A helper function to retrieve config values from the configuration file
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private static string GetConfigValue(string configValue, string defaultValue)
        {
            string a = String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
            return String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
        }

        /// <summary>
        /// Verifies input parameters for page size and page index.
        /// Called by GetAllProfiles, GetAllInactiveProfiles,
        /// FindProfilesByUserName, and FindInactiveProfilesByUserName.
        /// </summary>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        private void CheckParameters(int pageIndex, int pageSize)
        {
            if (pageIndex < 0) throw new ArgumentException("Page index must 0 or greater.");
            if (pageSize < 1) throw new ArgumentException("Page size must be greater than 0.");
        }

        private IEnumerable<user_entity> FindUsersByName(string usernameToMatch, DateTimeOffset userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor); ;
            IEnumerable<user_entity> userEntities = userDataSource.FindUsers(usernameToMatch, ApplicationName);

            List<user_entity> users = new List<user_entity>();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            userEntities.OrderBy(x => x.username);
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(userEntity);
            }

            return users;
        }

        private IEnumerable<user_entity> GetInactiveProfiles(IEnumerable<user_entity> userEntities, List<string> usernameActiveListsFounded)
        {
            List<user_entity> inactiveProfiles = new List<user_entity>();
            foreach (user_entity user in userEntities)
            {
                if (usernameActiveListsFounded.Contains(user.username))
                {
                    usernameActiveListsFounded.Remove(user.username);
                }
                else
                {
                    inactiveProfiles.Add(user);
                }
            }
            return inactiveProfiles;
        }

        private ProfileInfo GetProfileInfoFromEntity(user_entity userEntity)
        {
            return new ProfileInfo(userEntity.username, false, userEntity.last_activity_date.DateTime, userEntity.last_profile_updated_date.DateTime, 3);
        }

        private ProfileInfo GetProfileInfoFromUserEnity(user_entity userEntity)
        {
            const bool isAnonymous = false;
            const int profileInfoSize = 0;

            // ProfileInfo.Size not currently implemented.
            return new ProfileInfo(userEntity.username, isAnonymous, userEntity.last_activity_date.DateTime, userEntity.last_profile_updated_date.DateTime, profileInfoSize);
        }

        #endregion | Private Methods |
    }
}